function cssOrientationLockEvaluate(node, options, virtualNode, context) {
	const { cssom = undefined } = context || {};
	const { degreeThreshold = 0 } = options || {};
	if (!cssom || !cssom.length) {
		return undefined;
	}

	let isLocked = false;
	let relatedElements = [];
	const rulesGroupByDocumentFragment = groupCssomByDocument(cssom);

	for (const key of Object.keys(rulesGroupByDocumentFragment)) {
		const { root, rules } = rulesGroupByDocumentFragment[key];
		const orientationRules = rules.filter(isMediaRuleWithOrientation);
		if (!orientationRules.length) {
			continue;
		}

		orientationRules.forEach(({ cssRules }) => {
			Array.from(cssRules).forEach(cssRule => {
				const locked = getIsOrientationLocked(cssRule);

				// if locked and not root HTML, preserve as relatedNodes
				if (locked && cssRule.selectorText.toUpperCase() !== 'HTML') {
					const elms =
						Array.from(root.querySelectorAll(cssRule.selectorText)) || [];
					relatedElements = relatedElements.concat(elms);
				}

				isLocked = isLocked || locked;
			});
		});
	}

	if (!isLocked) {
		return true;
	}
	if (relatedElements.length) {
		this.relatedNodes(relatedElements);
	}
	return false;

	/**
	 * Group given cssom by document/ document fragment
	 * @param {Array<Object>} allCssom cssom
	 * @return {Object}
	 */
	function groupCssomByDocument(cssObjectModel) {
		return cssObjectModel.reduce((out, { sheet, root, shadowId }) => {
			const key = shadowId ? shadowId : 'topDocument';

			if (!out[key]) {
				out[key] = { root, rules: [] };
			}

			if (!sheet || !sheet.cssRules) {
				return out;
			}

			const rules = Array.from(sheet.cssRules);
			out[key].rules = out[key].rules.concat(rules);

			return out;
		}, {});
	}

	/**
	 * Filter CSS Rules that target Orientation CSS Media Features
	 * @param {Array<Object>} cssRules
	 * @returns {Array<Object>}
	 */
	function isMediaRuleWithOrientation({ type, cssText }) {
		/**
		 * Filter:
		 * CSSRule.MEDIA_Rule
		 * -> https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule
		 */
		if (type !== 4) {
			return false;
		}

		/**
		 * Filter:
		 * CSSRule with conditionText of `orientation`
		 */
		return (
			/orientation:\s*landscape/i.test(cssText) ||
			/orientation:\s*portrait/i.test(cssText)
		);
	}

	/**
	 * Interpolate a given CSS Rule to ascertain if orientation is locked by use of any transformation functions that affect rotation along the Z Axis
	 * @param {Object} cssRule given CSS Rule
	 * @property {String} cssRule.selectorText selector text targetted by given cssRule
	 * @property {Object} cssRule.style style
	 * @return {Boolean}
	 */
	function getIsOrientationLocked({ selectorText, style }) {
		if (!selectorText || style.length <= 0) {
			return false;
		}

		const transformStyle =
			style.transform || style.webkitTransform || style.msTransform || false;
		if (!transformStyle) {
			return false;
		}

		/**
		 * get last match/occurence of a transformation function that can affect rotation along Z axis
		 */
		const matches = transformStyle.match(
			/(rotate|rotateZ|rotate3d|matrix|matrix3d)\(([^)]+)\)(?!.*(rotate|rotateZ|rotate3d|matrix|matrix3d))/
		);
		if (!matches) {
			return false;
		}

		const [, transformFn, transformFnValue] = matches;
		let degrees = getRotationInDegrees(transformFn, transformFnValue);
		if (!degrees) {
			return false;
		}
		degrees = Math.abs(degrees);

		/**
		 * When degree is a multiple of 180, it is not considered an orientation lock
		 */
		if (Math.abs(degrees - 180) % 180 <= degreeThreshold) {
			return false;
		}

		return Math.abs(degrees - 90) % 90 <= degreeThreshold;
	}

	/**
	 * Interpolate rotation along the z axis from a given value to a transform function
	 * @param {String} transformFunction CSS transformation function
	 * @param {String} transformFnValue value applied to a transform function (contains a unit)
	 * @returns {Number}
	 */
	function getRotationInDegrees(transformFunction, transformFnValue) {
		switch (transformFunction) {
			case 'rotate':
			case 'rotateZ':
				return getAngleInDegrees(transformFnValue);
			case 'rotate3d':
				const [, , z, angleWithUnit] = transformFnValue
					.split(',')
					.map(value => value.trim());
				if (parseInt(z) === 0) {
					// no transform is applied along z axis -> ignore
					return;
				}
				return getAngleInDegrees(angleWithUnit);
			case 'matrix':
			case 'matrix3d':
				return getAngleInDegreesFromMatrixTransform(transformFnValue);
			default:
				return;
		}
	}

	/**
	 * Get angle in degrees from a transform value by interpolating the unit of measure
	 * @param {String} angleWithUnit value applied to a transform function (Eg: 1turn)
	 * @returns{Number|undefined}
	 */
	function getAngleInDegrees(angleWithUnit) {
		const [unit] = angleWithUnit.match(/(deg|grad|rad|turn)/) || [];
		if (!unit) {
			return;
		}

		const angle = parseFloat(angleWithUnit.replace(unit, ``));
		switch (unit) {
			case 'rad':
				return convertRadToDeg(angle);
			case 'grad':
				return convertGradToDeg(angle);
			case 'turn':
				return convertTurnToDeg(angle);
			case 'deg':
			default:
				return parseInt(angle);
		}
	}

	/**
	 * Get angle in degrees from a transform value applied to `matrix` or `matrix3d` transform functions
	 * @param {String} transformFnValue value applied to a transform function (contains a unit)
	 * @returns {Number}
	 */
	function getAngleInDegreesFromMatrixTransform(transformFnValue) {
		const values = transformFnValue.split(',');

		/**
		 * Matrix 2D
		 * Notes: https://css-tricks.com/get-value-of-css-rotation-through-javascript/
		 */
		if (values.length <= 6) {
			const [a, b] = values;
			const radians = Math.atan2(parseFloat(b), parseFloat(a));
			return convertRadToDeg(radians);
		}

		/**
		 * Matrix 3D
		 * Notes: https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix
		 */
		const sinB = parseFloat(values[8]);
		const b = Math.asin(sinB);
		const cosB = Math.cos(b);
		const rotateZRadians = Math.acos(parseFloat(values[0]) / cosB);
		return convertRadToDeg(rotateZRadians);
	}

	/**
	 * Convert angle specified in unit radians to degrees
	 * See - https://drafts.csswg.org/css-values-3/#rad
	 * @param {Number} radians radians
	 * @return {Number}
	 */
	function convertRadToDeg(radians) {
		return Math.round(radians * (180 / Math.PI));
	}

	/**
	 * Convert angle specified in unit grad to degrees
	 * See - https://drafts.csswg.org/css-values-3/#grad
	 * @param {Number} grad grad
	 * @return {Number}
	 */
	function convertGradToDeg(grad) {
		grad = grad % 400;
		if (grad < 0) {
			grad += 400;
		}
		return Math.round((grad / 400) * 360);
	}

	/**
	 * Convert angle specifed in unit turn to degrees
	 * See - https://drafts.csswg.org/css-values-3/#turn
	 * @param {Number} turn
	 * @returns {Number}
	 */
	function convertTurnToDeg(turn) {
		return Math.round(360 / (1 / turn));
	}
}

export default cssOrientationLockEvaluate;
